#include "config.h"

#include "edb.h"
#include "egid.h"

#define DEFAULT_SESSION_TIMEOUT	180 /* in secs */

static ecm_mutex_t	mutex;
static ecm_cond_t	cond_idle, cond_reconnected;
static ECMBOOL		reconnecting = ECM_FALSE;
static edb_t	*edb;
static ecm_uint32	edb_count;

static struct list_head	session_head;
static ecm_hashtable_t	sessions;

extern char	*edb_username;

static ECMBOOL
get_edb_connection(ECMBOOL retry)
{
	ecm_mutex_lock(mutex);
	if (edb == NULL) {
		if (edb_connect(NULL, ebc_port, edb_username, "egid", &edb) != EDB_OK) {
			ecm_mutex_unlock(mutex);
			return ECM_FALSE;
		}
	}
	else if (retry) {
		while (edb_count > 0)
			ecm_cond_wait(cond_idle, mutex, NULL);
		if (reconnecting)
			ecm_cond_wait(cond_reconnected, mutex, NULL);
		else {
			reconnecting = ECM_TRUE;
			if (edb_reconnect(edb) != EDB_OK) {
				reconnecting = ECM_FALSE;
				ecm_mutex_unlock(mutex);
				return ECM_FALSE;
			}
			reconnecting = ECM_FALSE;
			ecm_cond_broadcast(cond_reconnected);
		}
	}
	edb_count++;
	ecm_mutex_unlock(mutex);
	return ECM_TRUE;
}

static void
put_edb_connection(void)
{
	ecm_mutex_lock(mutex);
	ECMASSERT(edb_count > 0);
	edb_count--;
	if (edb_count == 0)
		ecm_cond_broadcast(cond_idle);
	ecm_mutex_unlock(mutex);
}

ECMBOOL
egid_is_on_master(void)
{
	ECMBOOL	is_on_master = ECM_FALSE;

	if (edb_is_master(edb)) {
		is_on_master = ECM_TRUE;
	}
	else if (get_edb_connection(ECM_TRUE)) {
		if (edb_is_master(edb))
			is_on_master = ECM_TRUE;
		put_edb_connection();
	}
	return is_on_master;
}

sr_t *
egid_process(const char *cmd, const sr_t *sr_info)
{
	sr_t	*sr_res = NULL;
	ECMBOOL	retry = ECM_FALSE;

again:
	if (!get_edb_connection(retry))
		return NULL;

	if (edb_ecx(edb, cmd, sr_dup(sr_info), &sr_res) != EDB_OK) {
		if (!retry) {
			put_edb_connection();
			retry = ECM_TRUE;
			goto again;
		}
		sr_res = NULL;
	}
	put_edb_connection();
	return sr_res;
}

static session_t *
create_session(const char *username, const char *session_id)
{
	session_t	*session;

	session = ecm_malloc(sizeof(session_t));
	session->destroyed = ECM_FALSE;
	session->username = ecm_strdup(username);
	session->session_id = ecm_strdup(session_id);
	session->sr_active_dom_names = sr_create_dir();
	session->sr_uploads = sr_create_dir();
	session->sr_downloads = sr_create_dir();
	session->refcnt = 1;
	session->sr_dynamics = NULL;
	session->last_accessed = ecm_time();
	session->lazy_results = ecm_ptrs_new(4);
	session->ecx_state_hash = ecm_hashtable_create(16);

	list_add_tail(&session->list, &session_head);
	ecm_hashtable_str_insert(sessions, session, session->session_id);
	return session;
}

static void
clear_uploads(sr_t *sr_uploads)
{
	sr_t	*sr_upload;

	SR_FOREACH_CHILD(sr_uploads, NULL, sr_upload) {
		char	*path = sr_dupi_str(sr_upload, 1);
		char	*dirname;

		ecm_unlink(path);
		dirname = ecm_dirname(path);
		ecm_free(path);
		ecm_rmdir_force(dirname);
	}
	sr_free(sr_uploads);
}

static void
free_ptrs_sr(ecm_ptrs_t *ptrs)
{
	unsigned	i;

	for (i = 0; i < ptrs->len; i++) {
		sr_free((sr_t *)ptrs->pdata[i]);
	}
	ecm_ptrs_free(ptrs);
}

static void
free_hash_sr(ecm_hashtable_t hash)
{
	sr_t	*sr;
	ecm_ht_iterator_t	iterator;

	for (sr = (sr_t *)ecm_hashtable_str_first(hash, &iterator, NULL);
	     sr;
	     sr = (sr_t *)ecm_hashtable_str_next(hash, &iterator, NULL))
		sr_free(sr);
	ecm_hashtable_finalize(hash);
}

static void
free_session(session_t *session)
{
	if (session == NULL)
		return;
	list_del(&session->list);
	ecm_hashtable_str_remove(sessions, session->session_id);
	ecm_free(session->username);
	ecm_free(session->session_id);
	sr_free(session->sr_active_dom_names);
	clear_uploads(session->sr_uploads);
	sr_free(session->sr_dynamics);
	free_ptrs_sr(session->lazy_results);
	free_hash_sr(session->ecx_state_hash);
	ecm_free(session);
}

session_t *
egid_login(const char *username, const char *passwd)
{
	session_t	*session;
	char		*session_id;
	ECMBOOL		retry = ECM_FALSE;

	if (ecm_empty_str(username))
		return NULL;
again:
	if (!get_edb_connection(retry))
		return NULL;
	if (edb_auth_user(edb, username, passwd, &session_id) != EDB_OK) {
		if (!retry) {
			put_edb_connection();
			retry = ECM_TRUE;
			goto again;
		}
		put_edb_connection();
		return NULL;
	}
	put_edb_connection();

	ecm_mutex_lock(mutex);
	session = create_session(username, session_id);
	ecm_free(session_id);
	ecm_mutex_unlock(mutex);
	return session;
}

void
egid_logout(session_t *session)
{
	if (session == NULL)
		return;
	ecm_mutex_lock(mutex);
	session->destroyed = ECM_TRUE;
	ecm_mutex_unlock(mutex);
}

static void
clear_old_sessions(void)
{
	session_t	*session;
	struct list_head	*lp, *next;
	ecm_time_t	curtime;

	curtime = ecm_time();

	list_for_each_n(lp, &session_head, next) {
		session = list_entry(lp, session_t, list);
		if (curtime - session->last_accessed > session_timeout &&
		    session->refcnt == 0)
			free_session(session);
	}
}

void
release_session(session_t *session, ECMBOOL accessed)
{
	if (session == NULL)
		return;
	ecm_mutex_lock(mutex);
	ECMASSERT(session->refcnt > 0);
	if (accessed)
		session->last_accessed = ecm_time();
	session->refcnt--;
	if (session->refcnt == 0 && session->destroyed)
		free_session(session);
	ecm_mutex_unlock(mutex);
}

ecm_uint32
session_add_upfile(session_t *session, const char *DOM, const char *path)
{
	sr_t		*sr_upload;
	ecm_uint32	idx;

	sr_upload = sr_create_dir();
	sr_append(sr_upload, sr_create_str(ST_STR, DOM));
	sr_append(sr_upload, sr_create_str(ST_STR, path));

	ecm_mutex_lock(mutex);

	idx = sr_get_num_child(session->sr_uploads) + 1;
	sr_append(session->sr_uploads, sr_upload);

	ecm_mutex_unlock(mutex);
	return idx;
}

sr_t *
session_get_upfile(session_t *session, ecm_uint32 idx)
{
	sr_t	*sr_upload;

	if (idx == 0)
		return NULL;
	ecm_mutex_lock(mutex);
	sr_upload = sr_geti(session->sr_uploads, idx - 1);
	ecm_mutex_unlock(mutex);
	return sr_upload;
}

sr_t *
session_get_downloads(session_t *session)
{
	sr_t	*sr_downloads;

	ecm_mutex_lock(mutex);
	sr_downloads = sr_dup(session->sr_downloads);
	ecm_mutex_unlock(mutex);
	return sr_downloads;
}

void
session_update_downloads(session_t *session, sr_t *sr_downloads)
{
	if (sr_downloads == NULL)
		return;
	ecm_mutex_lock(mutex);
	sr_free(session->sr_downloads);
	session->sr_downloads = sr_dup(sr_downloads);
	ecm_mutex_unlock(mutex);
}

char *
session_get_download(session_t *session, ecm_uint32 idx, ebc_ndid_t *pndid)
{
	const char	*ndname;
	char		*vpath;
	sr_t		*sr_downloads, *sr_download;
	ebc_ndid_t	ndid_dl, ndid_my;

	ecm_mutex_lock(mutex);
	sr_downloads = session->sr_downloads;
	sr_download = sr_geti(sr_downloads, idx - 1);
	if (sr_download == NULL)
		return NULL;
	ndname = ebc_get_ndname(((struct _edb_t *)edb)->ebc);
	ebc_get_ndid(ndname, &ndid_my);
	ndid_dl = sr_geti_int(sr_download, 0);
	vpath = ecm_strdup(sr_geti_str(sr_download, 1));
	if (ndid_dl == EBC_NDID_SELF || ndid_dl == ndid_my)
		*pndid = EBC_NDID_SELF;
	else
		*pndid = ndid_dl;
	ecm_mutex_unlock(mutex);
	return vpath;
}

ecm_uint32
session_save_result(session_t *session, sr_t *sr_res)
{
	ecm_uint32	idx;

	ecm_mutex_lock(mutex);
	idx = ecm_ptrs_add(session->lazy_results, sr_res);
	ecm_mutex_unlock(mutex);

	return idx;
}

sr_t *
session_crop_result(session_t *session, ecm_uint32 lazyres_idx)
{
	sr_t	*sr_result;

	ecm_mutex_lock(mutex);
	sr_result = (sr_t *)ecm_ptrs_remove(session->lazy_results, lazyres_idx);
	ecm_mutex_unlock(mutex);

	return sr_result;
}

ecm_uint32
session_reg_ecx_page(session_t *session, const sr_t *csr_ecx_state)
{
	sr_t	*sr_ecx_state = sr_dup(csr_ecx_state);
	ecm_uint32	ecx_page_id;

	ECMASSERT(sr_ecx_state != NULL);

	ecm_mutex_lock(mutex);
	do {
		ecx_page_id = (ecm_uint32)ecm_rand();
	}
	while (ecx_page_id == 0 ||
	       !ecm_hashtable_insert(session->ecx_state_hash, sr_ecx_state,
				     sizeof(ecm_uint32), &ecx_page_id));

	sr_setc_int(sr_ecx_state, "ecx_page_id", ecx_page_id);

	ecm_mutex_unlock(mutex);

	return ecx_page_id;
}

void
session_unreg_ecx_page(session_t *session, ecm_uint32 ecx_page_id)
{
	sr_t	*sr_old_ecx_state;

	if (ecx_page_id == 0)
		return;

	ecm_mutex_lock(mutex);
	sr_old_ecx_state = ecm_hashtable_remove(session->ecx_state_hash, sizeof(ecm_uint32), &ecx_page_id);
	sr_free(sr_old_ecx_state);
	ecm_mutex_unlock(mutex);
}

sr_t *
session_get_ecx_state(session_t *session, ecm_uint32 ecx_page_id)
{
	sr_t	*sr_ecx_state;

	ecm_mutex_lock(mutex);
	sr_ecx_state = sr_dup((const sr_t *)ecm_hashtable_get(session->ecx_state_hash, sizeof(ecm_uint32), &ecx_page_id));
	ecm_mutex_unlock(mutex);

	return sr_ecx_state;
}

void
session_set_dynamics(session_t *session, sr_t *sr_dynamics)
{
	ecm_mutex_lock(mutex);
	sr_free(session->sr_dynamics);
	session->sr_dynamics = sr_dup(sr_dynamics);
	ecm_mutex_unlock(mutex);
}

ECMBOOL
session_update_dynamics(session_t *session, sr_t *sr_dynamics)
{
	sr_t	*sr_old_dynamics;
	ECMBOOL	need_reload = ECM_FALSE;
	ecm_int32	n_old_dynamics, n_new_dynamics;

	ecm_mutex_lock(mutex);
	sr_old_dynamics = session->sr_dynamics;
	n_new_dynamics = sr_get_num_child(sr_dynamics);
	n_old_dynamics = sr_get_num_child(sr_old_dynamics);
	if (n_new_dynamics > 0 && n_old_dynamics > 0 &&
	    n_new_dynamics != n_old_dynamics)
		need_reload = ECM_TRUE;
	else {
		sr_t	*sr_dynamic, *sr_old_dynamic;

		sr_old_dynamic = sr_first(sr_old_dynamics);
		SR_FOREACH_CHILD(sr_dynamics, NULL, sr_dynamic) {
			if (ecm_strcmp(sr_get_name(sr_dynamic),
				       sr_get_name(sr_old_dynamic)) != 0 ||
			    ecm_strcmp(sr_geti_str(sr_dynamic, 0),
				       sr_geti_str(sr_old_dynamic, 0)) != 0) {
				need_reload = ECM_TRUE;
				break;
			}
			sr_old_dynamic = sr_next(sr_old_dynamic);
		}
	}

	sr_free(session->sr_dynamics);
	session->sr_dynamics = sr_dup(sr_dynamics);

	ecm_mutex_unlock(mutex);

	return need_reload;
}

session_t *
egid_auth(const char *username, const char *session_id)
{
	session_t	*session;

	ecm_mutex_lock(mutex);

	if (ecm_empty_str(username)) {
		ecm_mutex_unlock(mutex);
		return NULL;
	}

	clear_old_sessions();
	session = ecm_hashtable_str_get(sessions, session_id);
	if (session == NULL) {
		ecm_mutex_unlock(mutex);
		return NULL;
	}

	list_del(&session->list);
	list_add_tail(&session->list, &session_head);
	session->refcnt++;
	ecm_mutex_unlock(mutex);
	return session;
}

void
init_egid_auth(void)
{
	sessions = ecm_hashtable_create(32);
	INIT_LIST_HEAD(&session_head);
	if (session_timeout == 0)
		session_timeout = DEFAULT_SESSION_TIMEOUT;
}

void
initialize_edb_connection(void)
{
	mutex = ecm_mutex_create(ECM_FALSE);
	cond_idle = ecm_cond_create();
	cond_reconnected = ecm_cond_create();
}

void
finalize_edb_connection(void)
{
	if (edb)
		edb_close(edb);
	ecm_mutex_free(mutex);
}
